﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Script.Serialization;
using System.Xml;

namespace Net.Formats.oEmbed
{
    /// <summary>
    /// Request object used to get a resource using oEmbed format
    /// </summary>
    public class Request
    {
        #region Private Fields

        IResourceLoader loader;
        Uri _oEmbedResourceUrl;

        #endregion

        #region Public Properties

        public Uri oEmbedResourceUrl
        {
            get { return this._oEmbedResourceUrl; }
        }

        #endregion

        #region Class Constructors

        /// <summary>
        /// Create oEmbed request object by specifying 
        /// oEmbed request url
        /// </summary>
        /// <param name="url">oEmbed request url, 
        /// that consists of HTTP API endpoint and a collection of required 
        /// query string parameters
        /// <example>http://flickr.com/services/oembed?url=http%3A//flickr.com/photos/bees/2362225867/&maxwidth=300&maxheight=400&format=json</example>
        /// </param>
        /// <exception cref="System.ArgumentException">
        /// Exception is raised if url parameter is not a valid url.
        /// </exception>
        public Request( string url ) : this(url, new ResourceLoader()) { }

        /// <summary>
        /// Create oEmbed request object by specifying 
        /// oEmbed request url
        /// </summary>
        /// <param name="url">oEmbed request url, 
        /// that consists of HTTP API endpoint and a collection of required 
        /// query string parameters
        /// <example>http://flickr.com/services/oembed?url=http%3A//flickr.com/photos/bees/2362225867/&maxwidth=300&maxheight=400&format=json</example>
        /// </param>
        /// <exception cref="System.ArgumentException">
        /// Exception is raised if url parameter is not a valid url.
        /// </exception>
        /// <param name="loader">
        ///  Resource loader object (wrapper class around WebResponse/WebRequest funcionality)
        ///  used mainly for unit testing purposes
        /// </param>
        public Request( string url, IResourceLoader loader )
        {
            this.loader = loader;

            if ( !Uri.TryCreate( url, UriKind.Absolute, out _oEmbedResourceUrl ) )
            {
                throw new ArgumentException( "Url is not a valid url", "url" );
            }
        }

        /// <summary>
        ///Create oEmbed request object by specifying 
        /// oEmbed request url 
        /// </summary>
        /// <param name="url">
        /// oEmbed request url, 
        /// that consists of HTTP API endpoint and a collection of required 
        /// query string parameters
        /// <example>http://flickr.com/services/oembed?url=http%3A//flickr.com/photos/bees/2362225867/&maxwidth=300&maxheight=400&format=json</example>
        /// </param>
        public Request( Uri url ) : this(url, new ResourceLoader()) { }

        /// <summary>
        ///Create oEmbed request object by specifying 
        /// oEmbed request url 
        /// </summary>
        /// <param name="url"> 
        /// oEmbed request url, 
        /// that consists of HTTP API endpoint and a collection of required 
        /// query string parameters
        /// <example>http://flickr.com/services/oembed?url=http%3A//flickr.com/photos/bees/2362225867/&maxwidth=300&maxheight=400&format=json</example>
        ///</param>
        /// <param name="loader">
        ///  Resource loader object (wrapper class around WebResponse/WebRequest funcionality)
        ///  used mainly for unit testing purposes
        /// </param>
        public Request( Uri url, IResourceLoader loader )
        {
            this.loader = loader;
            _oEmbedResourceUrl = url;
        }


        /// <summary>
        /// Create oEmbed request object by specifying 
        /// oEmbed request parameters, that will be used to generate oEmbed request url.
        /// If provider API endpoint is not specified, will try and autodiscover oEmbed resource
        /// url, if provider supports discovery.
        /// </summary>
        /// <param name="parameters">
        /// <see cref="Net.Formats.oEmbed.RequestParameters">
        /// HTTP API endpoint and standard parameters exposed as object properties
        /// </see>
        /// </param>        
        public Request( RequestParameters parameters ) : this( parameters, new ResourceLoader( ) ) { }

        /// <summary>
        /// Create oEmbed request object by specifying 
        /// oEmbed request parameters, that will be used to generate oEmbed request url
        /// If provider API endpoint is not specified, will try and autodiscover oEmbed resource
        /// url, if provider supports discovery.
        /// </summary>
        /// <param name="parameters">
        /// HTTP API endpoint and standard oEmbed request query string parameters
        /// exposed as object properties
        /// </param>
        /// <param name="loader">
        ///  Resource loader object (wrapper class around WebResponse/WebRequest funcionality)
        ///  used mainly for unit testing purposes
        /// </param>
        public Request( RequestParameters parameters, IResourceLoader loader )
        {
            this.loader = loader;

            if ( parameters.ProviderEndpoint == null )
            {
                _oEmbedResourceUrl = DiscoverResourceUrl( parameters.ResourceUrl, parameters.Format );
            }
            else
            {
                _oEmbedResourceUrl = parameters.GenerateUrl( );
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Create oEmbed response and return appropriate oEmbed response type 
        /// as a <see cref="Net.Formats.oEmbed.GenericResponse"> base abstract type.</see>
        /// To cast to the concrete oEmbed response type, use Type property of a returned object
        /// to check for its type.
        /// </summary>
        /// <returns>
        /// oEmbed response of a type as defined by 
        /// the oEmbed provider
        /// </returns>
        public GenericResponse CreateResponse( )
        {
            var responseParams = RequestResource( _oEmbedResourceUrl );
            return ResponseFactory.Create( responseParams );
        }

        /// <summary>
        /// Create oEmbed response and return appropriate oEmbed response type
        /// </summary>
        /// <typeparam name="T">Response type that's required to be returned</typeparam>
        /// <returns>oEmbed response type as defined by the oEmbed provider</returns>
        public T CreateResponse<T>( ) where T : GenericResponse, new( )
        {
            var responseParams = RequestResource( _oEmbedResourceUrl );
            return ResponseFactory.Create<T>( responseParams );
        }


        /// <summary> 
        /// Discover oEmbed url for a content resource using supplied content url.
        /// If url is not found an exception is raised.
        /// An oEmbed provider has to support discovery for this to work.
        /// </summary>
        /// <param name="contentUrl">Content url</param>
        /// <param name="format">Desired format</param>
        /// <returns>oEmbed resource url for a content.</returns>
        /// <exception cref="Net.Formats.ResourceNotFoundException">
        /// If provider doesn't expose oEmbed resource url(s) for discovery.
        /// </exception>
        Uri DiscoverResourceUrl( Uri contentUrl, ResponseFormat? format )
        {
            Uri oEmbedUrl = null;

            var content = loader.GetResourceContent( contentUrl );

            var linkPattern = @"<link( [^>]*type=\""{0}\""[^>]*)>";

            var formatType = ( format.HasValue && format == ResponseFormat.JSON ) ? @"application/json\+oembed" : @"(text/xml\+oembed|application/xml\+oembed)";

            var matches = Regex.Matches( content, string.Format( linkPattern, formatType ), RegexOptions.IgnoreCase );

            if ( matches != null && matches.Count > 0 )
            {
                var match = matches[0];
                if ( match.Groups.Count >= 2 )
                {
                    var link = match.Groups[1].Value;
                    var hrefMatch = Regex.Match( link, "href=\"([^\"]*)\"", RegexOptions.IgnoreCase );

                    if ( hrefMatch.Groups.Count == 2 )
                    {
                        var value = hrefMatch.Groups[1].Value;

                        if ( !Uri.TryCreate( value, UriKind.Absolute, out oEmbedUrl ) )
                        {
                            throw new ResourceNotFoundException( );
                        }
                    }
                }
            }
            else
            {
                throw new ResourceNotFoundException( );
            }
            return oEmbedUrl;
        }

        /// <summary>
        /// Retrieve oEmbed response and store its values into 
        /// a dictionary object.
        /// </summary>
        /// <param name="url">oEmbed resource url</param>
        /// <returns>oEmbed response stored as a dictionary of key value pairs </returns>
        IDictionary<string, object> RequestResource( Uri url )
        {
            var properties = new Dictionary<string, object>( );

            ResponseFormat format;

            var responseStream = loader.GetResourceType( url, out format );

            if ( format == ResponseFormat.XML )
            {
                using ( var xmlReader = XmlReader.Create( new MemoryStream( Encoding.UTF8.GetBytes( responseStream ) ) ) )
                {
                    while ( xmlReader.Read( ) )
                    {
                        if ( xmlReader.NodeType == XmlNodeType.Element
                            && xmlReader.LocalName.ToLower( ) != "oembed" )
                        {
                            properties.Add( xmlReader.LocalName, xmlReader.ReadString( ) );
                        }
                    }
                }
            }

            if ( format == ResponseFormat.JSON )
            {
                var jsonSerializer = new JavaScriptSerializer( );
                properties = jsonSerializer.DeserializeObject( responseStream ) as Dictionary<string, object>;
            }

            return properties;
        }

        #endregion
    }
}
